Skip to content

Conversation

@dbschmigelski
Copy link
Member

@dbschmigelski dbschmigelski commented Oct 6, 2025

Description

In our docs we call out a Custom Node Type which can be used to implement deterministic nodes. But this requires customers to know about stop_reason and construct the DEEPLY nested return type of MultiAgentResult which uses NodeResult which uses AgentResult - this is too in the weeds in my opinion.

To improve developer experience, we will vend a FunctionNode which handles the hard part.

new DX

def deterministic_function(task, invocation_state=None, **kwargs):
    ...



# Create nodes
agent = Agent()
function_node = FunctionNode(deterministic_function, "deterministic_function")
# Build graph
builder = GraphBuilder()
builder.add_node(agent, "agent")
builder.add_node(function_node, "deterministic_function")

The alternative here is to vend an abstract class. I am on the fence about this. An abstract class feels more "correct" but the DX of simply providing the method feels easiest to me. Very open to discussing.

Related Issues

N/A

Documentation PR

pending

Type of Change

New feature

Testing

How have you tested the change? Verify that the changes do not break functionality or introduce warnings in consuming repositories: agents-docs, agents-tools, agents-cli

  • I ran hatch run prepare

Checklist

  • I have read the CONTRIBUTING document
  • I have added any necessary tests that prove my fix is effective or my feature works
  • I have updated the documentation accordingly
  • I have added an appropriate example to the documentation to outline the feature, or no new docs are needed
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@dbschmigelski dbschmigelski marked this pull request as ready for review October 6, 2025 22:49
Copy link
Member

@Unshure Unshure left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with the devex of this approach, it is very clean. As for a abstract class, I think we can migrate to that if customers want it in the future, but good with this for now.

logger.debug("function_name=<%s> | executing function", self.name)

start_time = time.time()
span = self.tracer.start_multiagent_span(task, "function_node")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if we should be using this to record a function node:

def start_multiagent_span(

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? We use it in both swarm and graph with the name of the node?

https://github.com/search?q=repo%3Astrands-agents%2Fsdk-python%20start_multiagent_span&type=code

Copy link
Member

@Unshure Unshure Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, I flagged this because we are setting a field in this function named gen_ai.agent.name, and this is almost always not an agent. We should probably revisit how we do this in the other multi-agent cases, but not a blocker for this pr

"gen_ai.agent.name": instance,

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh sorry misunderstood the callout.

Looking at it now, honeslty use of gen_ai.agent.name seems inappropriate for graph and swarm too at the top level. It seems like these should all be using gen_ai.operation.name.

So I guess we can

  1. Keep gen_ai.agent.name for consistency
  2. Remove entirely until we need to add back
  3. introduce a start_multiagent_function_node_span

I'm leaning towards 1 for consistency, seems like you are too if I'm not mistaken

Copy link
Member

@Unshure Unshure Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets not block this pr, im fine with keeping it consistent for now. But lets create an issue to track the proper fix of this

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pgrayy
pgrayy previously approved these changes Oct 9, 2025
JackYPCOnline
JackYPCOnline previously approved these changes Oct 9, 2025
zastrowm
zastrowm previously approved these changes Oct 9, 2025
@codecov
Copy link

codecov bot commented Oct 10, 2025

Codecov Report

❌ Patch coverage is 97.87234% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/strands/multiagent/function_node.py 97.82% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

Unshure
Unshure previously approved these changes Oct 19, 2025

def __call__(
self, task: str | list[ContentBlock], invocation_state: dict[str, Any] | None = None, **kwargs: Any
) -> str | list[ContentBlock] | Message:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any way we can provide more flexibility here? This essentially gets them to write MultiAgentBase. I'd rather they return string or something serializable, and we adjust accordingly

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They can return a string though if they want? I added the | list[ContentBlock] | Message because I thought that was more flexible if you want to do more advanced things

"""
self.func = func
self.name = name
self.tracer = get_tracer()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO for later probably: streaming callables 😅 #961

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point let me take a look for the streaming

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants